package org.rambadger.command.model;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import org.axonframework.commandhandling.annotation.CommandHandler;
import org.axonframework.eventhandling.annotation.EventHandler;
import org.axonframework.eventsourcing.annotation.AbstractAnnotatedAggregateRoot;
import org.axonframework.eventsourcing.annotation.AggregateIdentifier;
import org.axonframework.eventsourcing.annotation.EventSourcedMember;
import org.rambadger.command.auth.PlatformRole;
import org.rambadger.command.command.*;
import org.rambadger.command.event.*;

import static org.rambadger.command.command.AddUploadedFile.AddSubmissionFile;
import static org.rambadger.command.command.AddUploadedFile.SetUserPortrait;
import static org.rambadger.command.command.DeleteUploadedFile.DeleteSubmissionFile;
import static org.rambadger.command.event.FileDeleted.SubmissionFileDeleted;
import static org.rambadger.command.event.FileDeleted.UserPortraitRemoved;
import static org.rambadger.command.event.FileUploaded.SubmissionFileUploaded;
import static org.rambadger.command.event.FileUploaded.UserPortraitChanged;
import static org.rambadger.command.model.SubmissionEntity.SubmissionState;

import javax.annotation.Nullable;
import java.util.*;

/**
 * Aggregate root for Users.
 */
public class UserAggregate extends AbstractAnnotatedAggregateRoot<String> {
    private static final long serialVersionUID = 830645920682700261L;

    @AggregateIdentifier
    private String username;
    private String emailAddress;
    private String passwordHash;
    private String firstName;
    private String lastName;

    private String portraitId = null;

    private int submissionPoints = 0;
    private int reviewPoints = 0;

    private int nextSubmissionIndex = 1;

    @EventSourcedMember
    private List<SubmissionEntity> submissions = new ArrayList<>();

    //Roles
    private boolean hasAccess = false;
    private boolean hasAdmin = false;
    private Map<String, Set<PlatformRole>> platformRoles = new HashMap<>();

    UserAggregate() {}

    /**
     * Creates a new User Aggregate.
     *
     * @param cmd the command
     */
    @CommandHandler
    public UserAggregate(RegisterNewUser cmd) {
        apply(new UserRegistered(cmd.getUsername(), cmd.getPasswordHash(), cmd.getEmailAddress(), cmd.getFirstName(), cmd.getLastName()));
    }

    /**
     * Creates a new user and immediately grants them admin access.
     *
     * @param cmd the command
     */
    @CommandHandler
    public UserAggregate(CreateAdminUser cmd) {
        apply(new UserRegistered(cmd.getUsername(), cmd.getPasswordHash(), cmd.getEmailAddress(), cmd.getFirstName(), cmd.getLastName()));
        apply(new SystemRoleChanged.AccessGranted(cmd.getUsername()));
        apply(new SystemRoleChanged.AdminGranted(cmd.getUsername()));
    }

    /**
     * Changes a user's password.
     *
     * @param cmd the command
     */
    @CommandHandler
    public void changePassword(ChangeUserPassword cmd) {
        if (! getPasswordHash().equals(cmd.getNewPasswordHash())) {
            apply(new UserPasswordChanged(cmd.getUsername(), cmd.getNewPasswordHash()));
        }
    }

    /**
     * Changes a user's email address.
     *
     * @param cmd the command
     * @see #changeProfile(org.rambadger.command.command.ChangeUserProfile)
     */
    @CommandHandler
    public void changeEmail(ChangeUserEmail cmd) {
        if (getEmailAddress().equalsIgnoreCase(cmd.getEmailAddress())) {
            throw new RuntimeException("New email address is same as the old one");
        }
        apply(new UserEmailChanged(cmd.getUsername(), cmd.getEmailAddress()));
    }

    /**
     * Changes profile fields other than email address.
     *
     * @param cmd the command
     * @see #changeEmail(org.rambadger.command.command.ChangeUserEmail)
     */
    @CommandHandler
    public void changeProfile(ChangeUserProfile cmd) {
        apply(new UserProfileChanged(cmd.getUsername(), cmd.getFirstName(), cmd.getLastName()));
    }

    /**
     * Sets a new profile portrait.
     *
     * @param cmd the command
     * @see #removeProfileImage(org.rambadger.command.command.DeleteUploadedFile.RemoveUserPortrait)
     */
    @CommandHandler
    public void setProfileImage(SetUserPortrait cmd) {
        apply(new UserPortraitChanged(cmd.getUsername(), cmd.getFilename(), cmd.getFileId()));
    }

    /**
     * Removes the current profile portrait
     *
     * @param cmd the command
     * @see #setProfileImage(org.rambadger.command.command.AddUploadedFile.SetUserPortrait)
     */
    @CommandHandler
    public void removeProfileImage(DeleteUploadedFile.RemoveUserPortrait cmd) {
        apply(new UserPortraitRemoved(cmd.getUsername()));
    }

    /**
     * Starts a new submission
     *
     * @param cmd the command
     */
    @CommandHandler
    public void makeSubmission(CreateSubmission cmd) {
        //NOTE: The action is pre-approved here
        int submissionId = nextSubmissionIndex++;
        apply(new SubmissionCreated(username, submissionId , cmd.getPlatformId(), cmd.getAssignmentId()));
        if (!cmd.getMessage().isEmpty()) {
            apply(new SubmissionMessageEdited(username, submissionId, cmd.getMessage()));
        }
    }

    /**
     * Edits the message attached to a submission
     *
     * @param cmd the command
     */
    @CommandHandler
    public void editSubmission(EditSubmissionMessage cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.ACTIVE);  //It can only be edited while active


        if (!submission.getSubmissionMessage().equals(cmd.getNewMessage())) {
            //Only bother applying an event if a change would actually take place
            apply(new SubmissionMessageEdited(cmd.getUsername(), cmd.getSubmissionId(), cmd.getNewMessage()));
        }
    }

    /**
     * Attaches a new file to a submission.
     *
     * @param cmd the command
     */
    @CommandHandler
    public void addSubmissionFile(AddSubmissionFile cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.ACTIVE); //Only while active

        Map<String, String> files = submission.getSubmissionFiles();
        if (files.containsKey(cmd.getFilename())) {
            throw new RuntimeException(String.format("File '%s' already exists on submission %s/%d.", cmd.getFilename(), username, cmd.getSubmissionId()));
        } else {
            apply(new SubmissionFileUploaded(cmd.getUsername(), cmd.getSubmissionId(), cmd.getFilename(), cmd.getFileId()));
        }
    }

    /**
     * Removes an attached file from the submission
     *
     * @param cmd the command
     */
    @CommandHandler
    public void deleteSubmissionFile(DeleteSubmissionFile cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.ACTIVE); //Only while active

        Map<String, String> files = submission.getSubmissionFiles();
        if (!files.containsKey(cmd.getFilename())) {
            throw new RuntimeException(String.format("File '%s' does not exist on submission %s/%d.", cmd.getFilename(), username, cmd.getSubmissionId()));
        } else {
            apply(new SubmissionFileDeleted(cmd.getUsername(), cmd.getSubmissionId(), cmd.getFilename()));
        }
    }

    /**
     * Makes the submission uneditable and available to be claimed for review.
     *
     * @param cmd  the command
     */
    @CommandHandler
    public void publishSubmission(PublishSubmission cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.ACTIVE);  //It can only be published while active

        apply(new SubmissionPublished(username, cmd.getSubmissionId(), submission.getPlatformId(), submission.getAssignmentId()));
    }

    /**
     * Claimes a submission for review
     *
     * @param cmd the command
     */
    @CommandHandler
    public void claimSubmission(ClaimSubmissionForReview cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.PUBLISHED);

        apply(new SubmissionClaimedForReview(username, cmd.getSubmissionId(), cmd.getReviewerId()));
    }

    /**
     * Edits the the reviewer's response message.
     *
     * @param cmd the command
     */
    @CommandHandler
    public void editReview(EditReviewMessage cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.CLAIMED);
        if (!submission.getReviewerId().equals(cmd.getReviewerId())) {
            throw new RuntimeException("Reviewer mismatch");
        }
        apply(new ReviewMessageEdited(cmd.getSubmitterId(), cmd.getSubmissionId(), cmd.getNewMessage()));
    }

    /**
     * The review is finished by the reviewer, and points are awarded
     *
     * @param cmd the command
     */
    @CommandHandler
    public void finalizeReview(FinalizeReview cmd) {
        SubmissionEntity submission = requireSubmission(cmd.getSubmissionId());
        submission.ensureState(SubmissionState.CLAIMED);
        if (!submission.getReviewerId().equals(cmd.getReviewerId())) {
            throw new RuntimeException("Reviewer mismatch");
        }
        //NOTE: THis does not apply points to the user, only the Submission
        //It goes to a saga that handles that
        apply(new ReviewFinalized(cmd.getSubmitterId(), cmd.getSubmissionId(), cmd.getReviewerId(), cmd.getSubmissionPointsAwarded(), cmd.getReviewPointsAwarded(), cmd.getOutcome()));

    }

    @CommandHandler
    public void awardSubmissionPoints(AwardPoints.AwardSubmissionPoints cmd) {
        apply(new PointsAwarded.SubmissionPointsAwarded(username, cmd.getPoints()));

    }

    @CommandHandler
    public void awardReviewPoints(AwardPoints.AwardReviewPoints cmd) {
        apply(new PointsAwarded.ReviewPointsAwarded(username, cmd.getPoints()));
    }

    @CommandHandler
    public void grantPlatformRole(ChangePlatformRole.GrantPlatformRole cmd) {
        if (!hasPlatformRole(cmd.getPlatform(), cmd.getRole())) {
            apply(new PlatformRoleChanged.PlatformRoleGranted(username, cmd.getPlatform(), cmd.getRole()));
        }
    }

    @CommandHandler
    public void revokePlatformRole(ChangePlatformRole.RevokePlatformRole cmd) {
        if (hasPlatformRole(cmd.getPlatform(), cmd.getRole())) {
            apply(new PlatformRoleChanged.PlatformRoleRevoked(username, cmd.getPlatform(), cmd.getRole()));
        }
    }

    @CommandHandler
    public void grantAccess(ChangeSystemRole.GrantAccess cmd) {
        if (hasAccess == false) apply(new SystemRoleChanged.AccessGranted(username));
    }

    @CommandHandler
    public void revokeAccess(ChangeSystemRole.RevokeAccess cmd) {
        if (hasAccess == true) apply(new SystemRoleChanged.AccessRevoked(username));
    }

    @CommandHandler
    public void grantAdmin(ChangeSystemRole.GrantAdmin cmd) {
        if (hasAccess == false) apply(new SystemRoleChanged.AccessGranted(username));
        if (hasAdmin == false) apply(new SystemRoleChanged.AdminGranted(username));
    }

    @CommandHandler
    public void revokeAdmin(ChangeSystemRole.RevokeAdmin cmd) {
        if (hasAdmin == true) apply(new SystemRoleChanged.AdminRevoked(username));

    }

    @EventHandler void on(UserRegistered event) {
        username = event.getUsername();
        passwordHash = event.getPasswordHash();
        emailAddress = event.getEmailAddress();
        firstName = event.getFirstName();
        lastName = event.getLastName();
    }

    @EventHandler void on(UserPasswordChanged event) {
        passwordHash = event.getNewPasswordHash();
    }

    @EventHandler
    void on(UserEmailChanged event) {
        username = event.getUsername();
        emailAddress = event.getEmailAddress();
    }

    @EventHandler void on(UserProfileChanged event) {
        assert username.equals(event.getUsername());
        if (event.getFirstName() != null) {
            firstName = event.getFirstName();
        }
        if (event.getLastName() != null) {
            lastName = event.getLastName();
        }
    }

    @EventHandler void on(UserPortraitChanged event) {
        this.portraitId = event.getFileId();
    }

    @EventHandler void on(UserPortraitRemoved event) {
        this.portraitId = null;
    }

    @EventHandler void on(SubmissionCreated event) {
        submissions.add(new SubmissionEntity(event.getSubmissionId(), event.getPlatformId(), event.getAssignmentId()));
        nextSubmissionIndex++;
    }

    @EventHandler void on(PointsAwarded.SubmissionPointsAwarded event) {
        this.submissionPoints += event.getPoints();
    }

    @EventHandler void on(PointsAwarded.ReviewPointsAwarded event) {
        this.submissionPoints += event.getPoints();
    }

    @EventHandler void on(PlatformRoleChanged.PlatformRoleGranted evt) {
        Set<PlatformRole> perPlatform = null;
        if (platformRoles.containsKey(evt.getPlatform())) {
            perPlatform = platformRoles.get(evt.getPlatform());
        } else {
            perPlatform = EnumSet.noneOf(PlatformRole.class);
            platformRoles.put(evt.getPlatform(), perPlatform);
        }

        perPlatform.add(evt.getRole());
    }

    @EventHandler void on(PlatformRoleChanged.PlatformRoleRevoked evt) {
        if (platformRoles.containsKey(evt.getPlatform())) {
            Set<PlatformRole> perPlatform = platformRoles.get(evt.getPlatform());
            perPlatform.remove(evt.getRole());
            if (perPlatform.isEmpty()) {
                platformRoles.remove(evt.getPlatform());
            }
        }
    }

    @EventHandler void on(SystemRoleChanged.AccessGranted evt) {
        this.hasAccess = true;
    }

    @EventHandler void on(SystemRoleChanged.AccessRevoked evt) {
        this.hasAccess = false;
    }

    @EventHandler void on(SystemRoleChanged.AdminGranted evt) {
        this.hasAdmin = true;
    }

    @EventHandler
    void on(SystemRoleChanged.AdminRevoked evt) {
        this.hasAdmin = false;
    }

    /**
     * Gets the username/id.
     *
     * @return the username
     */
    public String getUsername() {
        return username;
    }

    /**
     * Gets the password hash.
     *
     * @return the password hash
     */
    public  String getPasswordHash() {
        return passwordHash;
    }

    /**
     * @return the user's email address
     */
    public String getEmailAddress() {
      return emailAddress;
    }

    /**
     * @return the user's first name
     */
    public String getFirstName() {
       return firstName;
    }

    /**
     * @return the user's last name
     */
    public String getLastName() {
        return lastName;
    }

    /**
     * @return the number of submission points that the user has accumulated
     */
    public int getSubmissionPoints() {
        return submissionPoints;
    }

    /**
     * @return the number of review points that the user has accumulated
     */
    public int getReviewPoints() {
        return reviewPoints;
    }

    /**
     * @param submissionId the numberical is of the submission to return
     * @return the submission
     */
    public Optional<SubmissionEntity> getSubmission(final int submissionId) {
        return Iterables.tryFind(submissions, new Predicate<SubmissionEntity>() {
            @Override
            public boolean apply(@Nullable SubmissionEntity input) {
                return input != null && input.getSubmissionId() == submissionId;
            }
        });
    }

    /**
     * Returns the submission with the given submission id. If the submission does not exist, an exception is thrown to cancel the transaction.
     *
     * @param submissionId the submission id
     * @return the desired submission entity
     * @throws RuntimeException if the submission is not found
     */
    public SubmissionEntity requireSubmission(int submissionId) throws RuntimeException {
        Optional<SubmissionEntity> submission = getSubmission(submissionId);
        if (submission.isPresent()) {
            return submission.get();
        } else {
            throw new RuntimeException(String.format("Submission #%d for user '%s' does not exist.", submissionId, username));
        }
    }

    public boolean hasPlatformRole(String platform, PlatformRole role) {
        if (platformRoles.containsKey(platform)) {
            return platformRoles.get(platform).contains(role);
        } else {
            return false;
        }
    }
}
